Pythonでインスタンス変数の入力値をバリデーションする方法
今回はPythonのクラスでインスタンス変数の入力時にバリデーションチェックを行う方法を調べました。
ここでの入力値のバリデーションとは、
- コンストラクタ
__init__
内でのバリデーション setter
メソッド内でのバリデーション
を指します。
要は、どちらの入力時にも同様のバリデーションチェックが走るようなコードの書きたかったのですが、いくつかやり方があったのでまとめてみました。
方法1. 愚直に書く
- コンストラクタ
__init__
でバリデーションを書く - セッターメソッド
@age.setter
を持っているインスタンス変数(ここでは、age
)は、セッターメソッド内でバリデーションを書いて処理を共通化
コード
class User: def __init__(self, name: str, age: int): # コンストラクタ内でバリデーション処理を実装 if type(name) is not str: raise TypeError('name must be str') self.__name = name # セッターメソッド側でバリデーション処理を共通化 self.age = age @property def name(self): return self.__name @property def age(self): return self.__age @age.setter def age(self, val: int): if type(val) is not int: raise TypeError('age must be int') self.__age = val
動作確認
# 初期化 # 不正な値を入れてみる user = User(name=False, age=False) >>> TypeError: name must be str # 正しい値を入れてみる user = User(name='arai', age=31) # セッター呼び出し # nameの値を書き換えてみる user.name = 'seiichi' >>> AttributeError: can't set attribute # ageの値を書き換えてみる user.age = 17 >>> TypeError: int must be int user.age = '31'
所感
これでも問題はないのですが、コンストラクトとセッターでバリデーションチェックを行っており全体的なコードの見通しが悪いのかなと感じました。
方法2. attrsライブラリをつかう
「attrs ライブラリに自動バリデーション機能をデコレータで追加してみた」 を参考にさせて頂きました。
attrsを利用することで、より簡潔に書くことができます。
ソースコード
import attr @attr.s class User: name = attr.ib(validator=attr.validators.instance_of(str)) age = attr.ib(validator=attr.validators.instance_of(int))
動作確認
# 初期化 # 不正な値を入れてみる user = User(name=False, age=False) # >>> TypeError: ("'name' must be <class 'str'> (got False that is a <class 'bool'>).", Attribute(name='name', default=NOTHING, validator=<instance_of validator for type <class 'str'>>, repr=True, eq=True, order=True, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False), <class 'str'>, False) # 正しい値を入れてみる user = User(name='arai', age=31) # セッター呼び出し # nameの値を書き換えてみる user.name = 'seiichi' # ageの値を書き換えてみる user.age = 17 user.age = '31'
所感
参考にしたプログの方でも指摘されていますが、
attrs
では後からインスタンス変数が変更された場合(setter)では、バリデーションチェックが走らないという大きな問題があります。2年ほど前からissueに挙がっていますが、まだクローズされていません。
その他個人的に気になったのは、勝手にsetterメソッドが定義される点です。できれば、user.name = 'seiichi'
はエラーになってほしいです。
一応@attr.s(frozen=True)
をつけてオブジェクト自体をイミュータブルにする方法もありますが、やりたいこととは違います。
そもそもPythonではsetter
の有無にかかわらず変数や関数は参照可能なので、仮にsetter
を未定義にしても変数の変更を防げる保証はないため、あまりこだわりすぎる必要はないのですが。
方法3. pyfiledライブラリをつかう
先ほどのissueの質問者が作成したpyfileds
というライブラリを試してみました。
pyfieldsでは、型チェック、独自バリデーションなどを簡潔に書くことができます。
ソースコード
from pyfields import field, make_init class User(object): name:str = field(check_type=True, read_only=True, doc='name must be str') age:int = field(check_type=True, doc='age must be int') __init__ = make_init()
動作確認
# 初期化 # 不正な値を入れてみる user = User(name=False, age=False) >>> FieldTypeError: Invalid value type provided for 'User.name'. Value should be of type <class 'str'>. Instead, received a 'bool': False # 正しい値を入れてみる user = User(name='arai', age=31) # セッター呼び出し # nameの値を書き換えてみる user.name = 'seiichi' >>> ReadOnlyFieldError: Read-only field 'User.name' has already been initialized on instance <__main__.User object at 0x> and cannot be modified anymore. # ageの値を書き換えてみる user.age = 17 user.age = '31' >>> FieldTypeError: Invalid value type provided for 'User.age'. Value should be of type <class 'int'>. Instead, received a 'str': '31'
所感
リリースされて間もないですが、ドキュメントも結構しっかりしており、個人的には一番使いやすかったです。
更新頻度は結構高く、2019/11/17には安定版がでています。
いろいろできることも多いので、機会があったらここらへんもまとめてブログ化したいと思います。
まとめ
いかがだったでしょうか。
他にいい方法を知っているという方いれば是非おしえてください。